[设计模式]之四:再谈单例模式


设计模式系列目录

之前有写过单例模式:[设计模式]之三:单例模式,可以通过链接回顾一下。然后这次再多聊聊这个设计模式。

线程不安全的懒汉式

可能一提到单例模式,大多数人都会想到这么写:

public class Singleton {
    private static Singleton instance;

    private Singleton() {

    }

    public static Singleton getInstance(){
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

这个写法也是严格按照概念来的

让类自身负责保存它的唯一实例。这个类保证没有其它实例可以被创建,并且它可以提供一个访问该实例的方法。

但这个写法有一个问题:多线程情况下并行调用Singleton.getInstance(),会导致创建多个实例。

可以用代码做个简单验证:

new Thread(new Runnable() {

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println("s1 hashcode:" + Singleton.getInstance().hashCode()); 
    }
}).start();

new Thread(new Runnable() {

  @Override
  public void run() {
    // TODO Auto-generated method stub
    System.out.println("s2 hashcode:" + Singleton.getInstance().hashCode()); 
  }
}).start();

通过hashCode看到有时候获取到的两个实例并不相同。

线程安全的懒汉式

对于多线程情况,一个直接的办法就是使用同步锁synchronized

public static synchronized Singleton getInstance(){
  if (null == instance) {
    instance = new Singleton();
  }
  return instance;
}

再次执行测试程序,现在不论怎么执行,打印的哈希值都是一样的。这样很轻松地解决了同步问题。

BUG解掉了不代表工作就结束了,我们还没看程序性能呢。处理性能问题,首先要梳理程序逻辑。

  • 同步锁解决的是什么问题?

    是会创建多个实例

  • 如果保证了只有一个实例,后面get实例还有问题吗?

    没有

那么问题就出来了,同步锁保证同时只有一个线程访问它,这个功能只在实例未创建的时候需要。其他时间,我们认为它可以随便调用。否则的话多线程运行到这里就成单线程了,一个一个执行,太影响效率了。

所以就需要先做一个判断,如果实例未创建,加锁!否则就返回实例。这样就形成了双重检验。

双重检验锁

public static Singleton getInstance(){
  if (null == instance) {
    synchronized (Singleton.class) {
      if (null == instance) {
        instance = new Singleton();
      }
    }
  }
  return instance;
}

首先,检查实例是否存在,不存在就进行加锁。但是加锁后,里面为何又要判断实例为空呢?因为可能有多线程一起通过第一层,而到了同步锁这里,几个线程排队执行。这样第一个线程创建实例后,第二个线程就会进来。所以需要再做一次判断,防止多创建实例。

到此,一个近乎完美的单例就写出来了。说是近乎,是因为还有一个很细小问题没有解决:指令顺序优化。

instance = new Singleton();

这行代码,在JVM中执行:(描述可能不是很准确,后续再修改)

  1. 为instance分配内存
  2. 调用Singleton构造函数初始化成员变量
  3. 将创建的对象指向分配的内存空间

执行了1,2,3后,instance才是非空的。但是在JVM编译时,为了提高执行效率,这些指令执行顺序会被调整。如果出现1,3,2的执行顺序,那么程序就会报错。

解决方案就是使用volatile关键字禁止指令排序优化。

private volatile static Singleton instance;

关于volatile关键字,是涉及到JVM书籍里都会讲到的。我会另外去总结,这里不多BB了。

饿汉式

前面的都是在第一次使用的时候,初始化实例,所以叫做懒汉式。那么饿汉式就是在使用前直接创建实例给你用。

private static final Singleton instance = new Singleton();
public static Singleton getInstance(){
  return instance;
}

其实这样更简单呢。

因为static final在第一次加载到内存就执行了,所以不用担心线程问题。

但是毕竟这样还是有缺陷的,就是如果涉及到根据参数初始化的情况,这个写法就不奏效了。

静态内部类

这个是推荐的写法。

private static class SingletonHolder {
  private static final Singleton INSTANCE = new Singleton();
} 

public static final Singleton getInstance(){
  return SingletonHolder.INSTANCE;
}

后面也会再聊聊内部静态类这个东西。

枚举单例

private static enum EnumSingleton {
  INSTANCE;

  private Singleton singleton;
  private EnumSingleton() {
    singleton = new Singleton();
  }
  private Singleton getInstance() {
    return singleton;
  }
}

private Singleton() {

}

public static Singleton getInstance(){
  return EnumSingleton.INSTANCE.getInstance();
}

目前最为安全的实现单例的方法是通过内部静态enum的方法来实现,因为JVM会保证enum不能被反射并且构造器方法只执行一次。

枚举类型可以干很多有意思的事情,后面也会拿出来聊一聊。


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
  目录